iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
AI Engineering

30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察系列 第 10

Day 10|故意把訊號弄壞:Artifact 注入實驗

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

Day 9 把 PPG 的 pulse interval 和 ECG 的 R-R 正面比過一次。平均間期只差 1.01 倍,清理後的 RMSSD 卻差 1.89 倍。問題是那個比值拆不開:用時間誤差的二階差公式去預測,預測比實測高了 145 ms。公式是代數恆等式,對不上的是前提——它假設兩邊的拍一對一、只差一個時間誤差,而真實紀錄裡同時有抖動、漏拍、假峰與錯配。誤差確實進來了,但看不出是從哪一道門進來的。

真實資料回答不了這個問題,因為沒有 ground truth。所以今天反過來做:拿 PhysioNet nsr2db 的 ECG 導出間期當基底,先把它清乾淨,再一次只注入一種已知類型、已知位置、已知強度的錯誤。

Day 8 與 Day 9 也注入過,但都是單一情境的事件型錯誤。今天補三件事:峰定位抖動強度梯度與跨 seed 的分佈,以及用已知的注入位置回頭算清理規則的 precision 與 recall——Day 9 在真實資料上排除了 54.3% 的 PPG 間期,但沒有判準能說它剔對了。

產出是可重現的 artifact 情境、指標偏差表與失敗案例,讓 Day 11 的 signal quality 規則有具體的 regression test,而不是只靠「看起來合理」判斷。

Concept

1. 五種錯誤,五種形態

注入之前得先分清楚每種錯誤改變的是什麼。

  • 峰定位抖動:峰的時間被推前或推後,相鄰兩個間期一長一短。序列長度不變。
  • 漏拍:漏掉一個峰,兩個間期併成一個接近兩倍的長間期。長度 −1。
  • 假峰:重搏切跡或雜訊被當成峰,一個間期被切成兩個短的。長度 +1。
  • 早發拍:真的心跳,但不從竇房結來。一個偏短的間期跟著一個代償偏長的。長度不變。
  • 訊號空檔:一整段讀不出來,留下一個遠超過生理範圍的長間期。

前三種是量測失誤,第四種是生理事件,第五種是覆蓋率缺口;五種都不改變總時長,這是注入前後還能比的前提。第五種是這個專案被咬過的那一種:Day 4 那 3 個 4.7–7.5 秒間期兩端都是竇性搏動,「剔除非竇性拍」擋不掉,全段 RMSSD 被抬高 53%。

2. 抖動要加在峰上,不是加在間期上

這是今天最容易做錯、而且錯了也不會報錯的一步。峰時間的誤差 e_i 進到間期時變成 e(i+1) − e(i),所以相鄰間期的誤差是負相關的——那才是「一長一短」。如果直接對每個間期獨立加噪音,相鄰誤差變成獨立的,形態就不對了。

兩者差三倍:

加在哪一層 SDNN² 增量 RMSSD² 增量 相鄰間期誤差的 lag-1 自相關
峰時間(一階差、二階差) 2σ² 6σ² −0.5
間期(少差一次) σ² 2σ² 0

要模擬峰定位誤差只能用峰時間層,用間期層會少掉三分之二的影響。

https://ithelp.ithome.com.tw/upload/images/20260923/201842061MqZaGBuPp.png
相鄰間期誤差的 lag-1 散佈與變異數比:峰時間層 ρ₁ = −0.50,間期層 0.00

3. 傷害大小在跑之前就算得出來

單次注入對 RMSSD 分子 Σd² 與 SDNN 分子 Σ(x−x̄)² 的貢獻,以平均間期 RR̄ 的平方為單位:

注入 Σd²(RMSSD) Σ(x−x̄)²(SDNN) 比值
漏拍 2.00 1.00 2.0
假峰(切點 0.9) 1.46 0.82 1.8
早發拍(提前 0.4) 0.96 0.32 3.0
假峰(切點 0.7) 0.74 0.58 1.3
假峰(切點 0.5) 0.50 0.50 1.0

假峰的破壞力取決於切在哪裡:對半切最輕,越偏離中點越重(0.5 到 0.9 差近三倍)。本實驗用 0.7 當偏離中點的切點,沒有量測真實重搏切跡落在哪——預設 0.5 是不是低估,取決於這個還沒驗證的前提。最右欄則說明**「RMSSD 比 SDNN 脆弱」不是通則**,取決於錯誤形態。

4. 偏差要除以 N,所以視窗長度是口徑的一部分

上面是分子。RMSSD² 的分母是有效差分數,所以同一次注入在短序列上是災難、在長序列上看不見。不寫 N,偏差表不能解讀——Task Force 1996 也明說不同長度的紀錄不可互相比較時域指標。

5. 強度要有共同軸

注入率(每百拍幾個事件)和抖動幅度(σ 毫秒)是不同單位,並排比不出「哪一種最傷」。用第 3 節的係數把兩邊換算成每拍注入多少變異就對齊了:事件是注入率乘以單次係數,抖動是 6(σ/RR̄)²。對齊之後才問得出今天真正想問的那題:在預測傷害相同的強度下,清理規則的表現一樣嗎?事件是離散的、形態判準有明確的壞拍可抓;抖動是連續的,沒有哪一拍算壞拍——門檻照樣會大量剔除間期,卻消不掉抖動造成的偏差。

6. 有 ground truth 才能量清理規則

注入位置是已知的,所以清理不只能看「指標有沒有回到真值」,還能直接算召回率與誤剔率——指標變好也可能只是因為最難的那些被丟掉了。這是合成資料唯一能給、真實資料給不了的東西。

抖動情境特別有意思:它沒有「該剔的間期」,每一拍都是真的心跳,所以規則在抖動下剔掉的每一個,定義上都是誤剔。

Hands-on

Dataset

PhysioNet nsr2db 的 nsr001,ECG 導出的 R-R 標註。取兩端都是竇性的間期(NN),再套 Day 4 更正後的規則剔掉超過 3 秒的訊號空檔:106298 → 106295 個間期,RR̄ 760.5 ms、RMSSD 32.10 ms、SDNN 168.54 ms。這是注入前的參照,不是生理真值。

Method

五種錯誤各沿同一條「每拍注入多少變異」的軸走 8 格(1e-4 到 1e-1),每格 10 個 seed,共 400 列。全部在峰時間層注入,同時記下哪些間期被弄壞。每列三個口徑並排:真值、注入後不清理、注入後清理(clean_rr 加上 gap-aware RMSSD)。

有三格不是量測結果,排除在統計與圖之外:jitter 在 0.1 那格有 4 個 seed 讓峰時間交錯(見 Limitations);signal_gap 在最低兩格事件數 round 成 0——偏差比恆等於 1.00,看起來像「低強度的空檔無害」,其實是沒有空檔。

Code

落點不能隨便挑。解析係數假設事件周圍是等間距,所以每個事件弄壞的間期,兩側都要留一個乾淨的間期——而「footprint」每種錯誤不一樣:

def _event_bounds(kind, peaks, shape):
    """回傳 (最早落點, 最晚落點, 事件間最小間距)。"""
    n = peaks.size
    if kind in ("missed_beat", "ectopic"):
        return 2, n - 3, 3           # 各弄壞兩個相鄰差分
    if kind == "extra_beat":
        return 1, n - 3, 2           # 只切開一個間期
    span = _gap_span(peaks, shape)   # 空檔吃掉幾拍,用實際峰時間算
    return 2, n - 2 - span, span + 2

寫錯的代價是靜默的:漏拍貼在序列首尾時實測 Σd² 只有 1.00(預測 2.00),兩個漏拍相隔 2 個峰時是 2.00(預測 4.00)。

結果與意外

原本以為 實際發現
RMSSD 對差分取平方,所以比 SDNN 脆弱 要看錯誤形態。單次早發拍對 RMSSD 與 SDNN 分子的貢獻比是 3 倍,對半切的假峰卻是 1 倍;不能只憑指標名稱排序。
漏拍與假峰都是「多一個/少一個峰」,破壞力應該差不多 單次漏拍對 RMSSD 分子 Σd² 的預測貢獻是 2.00 RR̄²,對半切假峰只有 0.50 RR̄²,差四倍;假峰切點越偏,傷害越大。
把抖動加在間期上或加在峰時間上,只是實作細節 峰時間誤差會連續差分兩次,RMSSD² 的預測增量是 6σ²;直接在間期加獨立誤差只有 2σ²,少算三分之二。
注入錯誤會讓 HR 和 HRV 一起失真 在事件型錯誤的 target=0.1 那格,HR 最多偏 13.5%,RMSSD 卻變成真值的約 7–8 倍;但高強度 jitter 清理後 HR 也會從 78.96 掉到 77.06 bpm(0.037 那格,剔除率 32%),不能說 HR 永遠穩。
清理後指標回到真值,就代表清理規則抓對了地方 target=0.1 那格四種事件型錯誤清理後 RMSSD 都約 30.4 ms(真值 32.10 ms),precision 卻從 0.192 到 0.665。但 precision 低不等於剔錯多:precision 最低的 signal_gap 誤剔率只有 0.005,最高的 ectopic 反而是 0.132——precision 的分母還含事件數與基底既有的誤剔,要配著誤剔率一起讀。
固定 seed 就足以讓偏差數字可重現 單一 seed 讓錯誤重現,也讓落點 bug 穩定漏網:掃 seed 0–99 才發現尾端截短與事件重疊。改按事件 footprint 定界,再用 0–199 的 seed 驗證。
一個異常拍就足以毀掉指標 傷害還要除以有效差分數。Day 4 同一個早發拍在 n=300 時讓 RMSSD 變 2.13 倍,n=1200 時是 1.43 倍;不寫視窗長度,就無法解讀偏差。

https://ithelp.ithome.com.tw/upload/images/20260923/20184206PY5fUfaQcB.png
清理前後的 RMSSD 偏差:不清理時五條線大致對齊,清理後四種離散事件壓回 0.95、jitter 爬到 2.6

左圖是共同強度軸大致成立的證據——預測傷害相同,實測偏差就落在同一條帶裡(target=0.1 那格是真值的 7.26–8.14 倍),但不會精確相等:強度軸對齊的是平均傷害,消不掉事件形態與落點的差異。右圖才是今天的結果。

https://ithelp.ithome.com.tw/upload/images/20260923/201842063xiQubjCvn.png
同一預測傷害下的 recall、precision 與誤剔率,precision 旁標示 FP 裡有多少來自基底

這張圖是另一個強度格(target=0.01,比上表低一個數量級)。precision 那一條要配著基底讀:未注入時同一套規則就已經剔掉 400 個間期,而 signal_gap 的 411 個 FP 裡有 400 個是它,precision 0.028 幾乎不是在講空檔。事件越少,基底那 400 個佔的比例越大——上表 signal_gap 的 0.192 是同一個機制。

Limitations

  • 清理判定依賴鄰居。 jump_mask 看前一拍,local_outlier_mask 看 21 拍視窗;注入會連帶改變原本間期的判定。在 20000 拍切片中,jitter 讓基底原本被剔的 86 拍裡有 68–72 拍改為通過,約八成翻面。因此 jitter 的誤剔不能簡單扣掉基底的誤剔數;這裡只並列報告,不做相減。
  • 共同強度軸有不可達的頂端。 target=0.1 對應的 jitter σ=98 ms;nsr001 的最短間期只有 437.5 ms,10 個 seed 中有 4 個讓峰時間交錯。程式拒絕重排並記錄失敗,所以 jitter 圖只畫到 0.037。只平均成功的 seed 會偏向沒撞到短間期的樣本。
  • 基底與錯誤類型有限。 這裡的「真值」是先清理過的 ECG 間期,不是無誤的生理真值;一次只注入一種錯誤,也沒有模擬 PPG 波形上的動作雜訊。結果能檢查這條間期清理管線,不能直接推到混合 artifact 的實際佩戴情境。

這對 AI Engineering 的意義

一個清理後的 rmssd_ms 不足以證明資料品質。這次四種事件型錯誤都收斂到約 30.4 ms,precision 卻差很大;jitter 更是在剔掉 32% 的間期後,RMSSD 仍是真值的 2.6 倍。進入後續 AI 管線的 feature 應帶著有效間期數、剔除率、錯誤類型與清理規則版本;有注入 ground truth 的測試還要保存 recall、precision 和失敗的強度格,讓「數字看起來正常」可以被追問。

固定 seed 是重跑同一案例的起點,不是驗證充分的終點。這次落點 bug 由跨 seed 的邊界測試才揭露;不可達的 jitter 強度也提醒我,失敗案例不能直接丟掉再平均。Day 11 的 signal quality 規則應把這些案例做成 regression test,並在品質不足時讓下游選擇保留或拒答,而不是把清理後的數字當成確定的健康訊號。


上一篇
Day 09|PPG 推出的 R-R vs ECG 的 R-R:差多少?
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言